package org.xtext.runtime.type;

import java.util.HashMap;
import java.util.Map;

import org.xtext.runtime.term.ITerm;
import org.xtext.runtime.term.List;

public class Type implements IType {
	public static final IType booleanType = new Type(Boolean.class, "boolean");
	public static final IType characterType = new Type(Character.class, "char");
	public static final IType integerType = new Type(Integer.class, "int");
	public static final IType longType = new Type(Long.class, "long");
	public static final IType floatType = new Type(Float.class, "float");
	public static final IType doubleType = new Type(Double.class, "double");
	public static final IType stringType = new Type(String.class, "string");
	public static final IType listType = new Type(java.util.List.class, "list");
	
	public static final IType[] numericTypes = new IType[] {
		integerType,
		longType,
		floatType,
		doubleType
	};
	
	private Class<?> clazz;
	private String type;
	
	public Type() {
	}
	
	public Type(Class<?> clazz, String type) {
		this.clazz = clazz;
		this.type = type;
	}
	
	public boolean equals(Object obj) {
		if (obj instanceof Type) {
			Type t = (Type) obj;
			return clazz.equals(t.clazz) && type.equals(t.type);
		}
		return false;
	}
	
	public String type() {
		return type;
	}
	
	public String toString() {
		return type();
	}

	@Override
	public boolean typeCheck(Object obj) {
		return clazz.isAssignableFrom(obj.getClass());
	}

	@Override
	public boolean matches(IType type) {
		if (Type.class.isInstance(type)) {
			Type ptype = (Type) type;
			return clazz.equals(ptype.clazz) && type.equals(type);
		}
		return false;
	}

	public static boolean isNumericType(ITerm term) {
		for (IType type : numericTypes) {
			if (term.type().equals(type)) return true;
		}
		return false;
	}
	
	// Add this for long, float, double...
	public static int termAsInt(ITerm term) throws TypeException {
		Object obj = term.value();
		if (obj instanceof Number) {
			return ((Number) obj).intValue();
		}

		throw new TypeException("Could not convert: " + term + " to int");
	}

	public static long termAsLong(ITerm term) throws TypeException {
		Object obj = term.value();
		if (obj instanceof Number) {
			return ((Number) obj).longValue();
		}

		throw new TypeException("Could not convert: " + term + " to long");
	}

	public static float termAsFloat(ITerm term) throws TypeException {
		Object obj = term.value();
		if (obj instanceof Number) {
			return ((Number) obj).floatValue();
		}
		
		throw new TypeException("Could not convert: " + term + " to float");
	}

	public static double termAsDouble(ITerm term) throws TypeException {
		Object obj = term.value();
		if (obj instanceof Number) {
			return ((Number) obj).doubleValue();
		}
		
		throw new TypeException("Could not convert: " + term + " to double");
	}

	public static String termAsString(ITerm term) throws TypeException {
		if (List.class.isInstance(term)) {
			return term.toString();
		}
		
		Object obj = term.value();
		if (obj instanceof Number) {
			return obj.toString();
		}
		
		return obj.toString();
//		throw new TypeException("Could not convert: " + term + " to String");
	}

	/**
	 * Selects the more general type from the pair of types presented as arguments
	 * (e.g. double is the most general, and int is the most specific).
	 * 
	 * @param type
	 * @param type2
	 * @return
	 */
	public static IType selectNumericType(IType type, IType type2) {
		int i1 = -1;
		int i2 = -1; 
		for (int i = 0; i < numericTypes.length; i++) {
			if (numericTypes[i].equals(type)) i1 = i; 
			if (numericTypes[i].equals(type2)) i2 = i; 
		}
		
		if (i1 >= 0 && i2 >= 0) {
			return (i1 > i2) ? type : type2;
		}
		
		return null;
	}

	public static IType selectIntegerType(IType type, IType type2) {
		if (type.equals(Type.longType) || type2.equals(Type.longType)) {
			return Type.longType;
		}
		
		if (type.equals(Type.integerType) && type2.equals(Type.integerType)) {
			return Type.integerType;
		}

		return null;
	}

	private static Map<Class<?>, IType> typeMap = new HashMap<Class<?>, IType>();
	
	static {
		typeMap.put(int.class, Type.integerType);
		typeMap.put(Integer.class, Type.integerType);
		typeMap.put(long.class, Type.longType);
		typeMap.put(Long.class, Type.longType);
		typeMap.put(float.class, Type.floatType);
		typeMap.put(Float.class, Type.floatType);
		typeMap.put(double.class, Type.doubleType);
		typeMap.put(Double.class, Type.doubleType);
		typeMap.put(String.class, Type.stringType);
		typeMap.put(boolean.class, Type.booleanType);
		typeMap.put(Boolean.class, Type.booleanType);
		typeMap.put(char.class, Type.characterType);
		typeMap.put(Character.class, Type.characterType);
	}
	
	public static IType getType(Class<?> clazz) {
		// Any java.util.List implementation should satisfy the list type...
		if (java.util.List.class.isAssignableFrom(clazz)) {
			return Type.listType;
		}
		
		return typeMap.get(clazz);
	}
}
